package com.wayne.binding;

import cn.hutool.core.util.ClassUtil;
import com.wayne.annotations.Delete;
import com.wayne.annotations.Insert;
import com.wayne.annotations.Select;
import com.wayne.annotations.Update;
import com.wayne.config.XMLMapperBuilder;
import com.wayne.io.Resources;
import com.wayne.pojo.Configuration;
import com.wayne.pojo.MappedStatement;
import com.wayne.pojo.SqlCommandType;

import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MapperAnnotationBuilder {

  private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
      .of(Select.class, Update.class, Insert.class, Delete.class)
      .collect(Collectors.toSet());
  private final Configuration configuration;
  private final Class<?> type;

  public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    this.configuration = configuration;
    this.type = type;
  }

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      for (Method method : type.getMethods()) {
        parseStatement(method);
      }
    }
  }

  private Optional<AnnotationWrapper> getAnnotationWrapper(Method method,
                                                           Collection<Class<? extends Annotation>> targetTypes) {
    return targetTypes.stream()
        .flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new).findFirst();
  }

  void parseStatement(Method method) {
    final String mappedStatementId = type.getName() + "." + method.getName();
    if (configuration.hasStatement(mappedStatementId)) {
      return;
    }
    getAnnotationWrapper(method, statementAnnotationTypes).ifPresent(statementAnnotation -> {
      final Class<?> parameterTypeClass = getParameterType(method);
      MappedStatement mappedStatement = new MappedStatement();
      mappedStatement.setId(mappedStatementId);
      mappedStatement.setParameterType(parameterTypeClass != null ? parameterTypeClass.getName() : null);
      mappedStatement.setResultType(getReturnType(method ,type).getName());
      mappedStatement.setSqlCommandType(statementAnnotation.getSqlCommandType());
      mappedStatement.setSql(buildSql(statementAnnotation.getAnnotation()));
      configuration.getMappedStatementMap().put(mappedStatementId, mappedStatement);
    });
  }

  private String buildSql(Annotation annotation) {
    if (annotation instanceof Select) {
      return ((Select) annotation).value()[0];
    } else if (annotation instanceof Update) {
      return ((Update) annotation).value()[0];
    } else if (annotation instanceof Insert) {
      return ((Insert) annotation).value()[0];
    } else if (annotation instanceof Delete) {
      return ((Delete) annotation).value()[0];
    }
    return null;
  }

  private Class<?> getParameterType(Method method) {
    Class<?> parameterType = null;
    Class<?>[] parameterTypes = method.getParameterTypes();
    if (parameterTypes.length == 1 && !ClassUtil.isBasicType(parameterTypes[0])&& !ClassUtil.isPrimitiveWrapper(parameterTypes[0])) {
      parameterType = parameterTypes[0];
    } else {
      parameterType = MapperMethod.ParamMap.class;
    }
    return parameterType;
  }
  private static Class<?> getReturnType(Method method, Class<?> type) {
    Class<?> returnType = method.getReturnType();
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
    if (resolvedReturnType instanceof Class) {
      returnType = (Class<?>) resolvedReturnType;
      if (returnType.isArray()) {
        returnType = returnType.getComponentType();
      }
    } else if (resolvedReturnType instanceof ParameterizedType) {
      ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
      Class<?> rawType = (Class<?>) parameterizedType.getRawType();
      if (Collection.class.isAssignableFrom(rawType) ) {
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        if (actualTypeArguments != null && actualTypeArguments.length == 1) {
          Type returnTypeParameter = actualTypeArguments[0];
          if (returnTypeParameter instanceof Class<?>) {
            returnType = (Class<?>) returnTypeParameter;
          } else if (returnTypeParameter instanceof ParameterizedType) {
            // (gcode issue #443) actual type can be a also a parameterized type
            returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
          } else if (returnTypeParameter instanceof GenericArrayType) {
            Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
            // (gcode issue #525) support List<byte[]>
            returnType = Array.newInstance(componentType, 0).getClass();
          }
        }
      } else if (Optional.class.equals(rawType)) {
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        Type returnTypeParameter = actualTypeArguments[0];
        if (returnTypeParameter instanceof Class<?>) {
          returnType = (Class<?>) returnTypeParameter;
        }
      }
    }

    return returnType;
  }
  private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // #1347
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
      if (inputStream == null) {
        // Search XML mapper that is not in the module but in the classpath.
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(configuration);
        xmlParser.parse(inputStream);
      }
    }
  }

  private static class AnnotationWrapper {
    private final Annotation annotation;
    private final SqlCommandType sqlCommandType;

    AnnotationWrapper(Annotation annotation) {
      this.annotation = annotation;
      if (annotation instanceof Select) {
        sqlCommandType = SqlCommandType.SELECT;
      } else if (annotation instanceof Update) {
        sqlCommandType = SqlCommandType.UPDATE;
      } else if (annotation instanceof Insert) {
        sqlCommandType = SqlCommandType.INSERT;
      } else if (annotation instanceof Delete) {
        sqlCommandType = SqlCommandType.DELETE;
      } else {
        sqlCommandType = SqlCommandType.UNKNOWN;
      }
    }

    public Annotation getAnnotation() {
      return annotation;
    }

    public SqlCommandType getSqlCommandType() {
      return sqlCommandType;
    }
  }
}